{
  "cells": [
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# FSRS4Anki v6.1.3 Optimizer\n",
        "\n",
        "[![open in colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/open-spaced-repetition/fsrs4anki/blob/v6.1.3/fsrs4anki_optimizer.ipynb)\n",
        "\n",
        "↑ Click the above button to open the optimizer on Google Colab.\n",
        "\n",
        "> If you can't see the button and are located in the Chinese Mainland, please use a proxy or VPN."
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "wG7bBfGJFbMr"
      },
      "source": [
        "Upload your **Anki Deck Package (.apkg)** file or **Anki Collection Package (.colpkg)** file on the `Left sidebar -> Files`, drag and drop your file in the current directory (not the `sample_data` directory). \n",
        "\n",
        "No need to include media. Need to include scheduling information. \n",
        "\n",
        "> If you use the latest version of Anki, please check the box `Support older Anki versions (slower/larger files)` when you export.\n",
        "\n",
        "You can export it via `File -> Export...` or `Ctrl + E` in the main window of Anki.\n",
        "\n",
        "Then replace the `filename` with yours in the next code cell. And set the `timezone` and `next_day_starts_at` which can be found in your preferences of Anki.\n",
        "\n",
        "After that, just run all (`Runtime -> Run all` or `Ctrl + F9`) and wait for minutes. You can see the optimal parameters in section **2.3 Result**. Copy them, replace the parameters in `fsrs4anki_scheduler.js`, and paste them into the custom scheduling of your deck options (require Anki version >= 2.1.55).\n",
        "\n",
        "**NOTE**: The default output is generated from my review logs. If you find the output is the same as mine, maybe your notebook hasn't run there.\n",
        "\n",
        "**Contribute to SRS Research**: If you want to share your data with me, please fill this form: https://forms.gle/KaojsBbhMCytaA7h8"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "id": "iqP70_-3EUhi"
      },
      "outputs": [],
      "source": [
        "# Here are some settings that you need to replace before running this optimizer.\n",
        "\n",
        "filename = \"collection-2022-09-18@13-21-58.colpkg\"\n",
        "# If you upload deck file, replace it with your deck filename. E.g., ALL__Learning.apkg\n",
        "# If you upload collection file, replace it with your colpkg filename. E.g., collection-2022-09-18@13-21-58.colpkg\n",
        "\n",
        "# Replace it with your timezone. I'm in China, so I use Asia/Shanghai.\n",
        "# You can find your timezone here: https://gist.github.com/heyalexej/8bf688fd67d7199be4a1682b3eec7568\n",
        "timezone = 'Asia/Shanghai'\n",
        "\n",
        "# Replace it with your Anki's setting in Preferences -> Scheduling.\n",
        "next_day_starts_at = 4\n",
        "\n",
        "# Replace it if you don't want the optimizer to use the review logs before a specific date.\n",
        "revlog_start_date = \"2006-10-05\"  # YYYY-MM-DD\n",
        "\n",
        "# Set it to True if you don't want the optimizer to use the review logs from suspended cards.\n",
        "filter_out_suspended_cards = False\n",
        "\n",
        "# Red: 1, Orange: 2, Green: 3, Blue: 4, Pink: 5, Turquoise: 6, Purple: 7\n",
        "# Set it to [1, 2] if you don't want the optimizer to use the review logs from cards with red or orange flag.\n",
        "filter_out_flags = []\n",
        "\n",
        "enable_short_term = True\n",
        "\n",
        "recency_weight = True"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "bLFVNmG2qd06"
      },
      "source": [
        "## 1 Build dataset"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "EkzFeKawqgbs"
      },
      "source": [
        "### 1.1 Extract Anki collection & deck file"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "KD2js_wEr_Bs",
        "outputId": "42653d9e-316e-40bc-bd1d-f3a0e2b246c7"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Note: you may need to restart the kernel to use updated packages.\n",
            "Deck file extracted successfully!\n",
            "revlog.csv saved.\n"
          ]
        }
      ],
      "source": [
        "%pip install -q fsrs_optimizer==6.1.5\n",
        "# for local development\n",
        "# import os\n",
        "# import sys\n",
        "# sys.path.insert(0, os.path.abspath('../fsrs-optimizer/src/fsrs_optimizer/'))\n",
        "import fsrs_optimizer as optimizer\n",
        "optimizer = optimizer.Optimizer(enable_short_term=enable_short_term)\n",
        "optimizer.anki_extract(filename, filter_out_suspended_cards, filter_out_flags)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "dKpy4VfqGmaL"
      },
      "source": [
        "### 1.2 Create time-series feature & analysis\n",
        "\n",
        "The following code cell will extract the review logs from your Anki collection and preprocess them to a trainset which is saved in [./revlog_history.tsv](./revlog_history.tsv).\n",
        "\n",
        "The time-series features are important in optimizing the model's parameters. For more detail, please see my paper: https://www.maimemo.com/paper/\n",
        "\n",
        "Then it will generate a concise analysis for your review logs. \n",
        "\n",
        "- The `r_history` is the history of ratings on each review. `3,3,3,1` means that you press `Good, Good, Good, Again`. It only contains the first rating for each card on the review date, i.e., when you press `Again` in review and  `Good` in relearning steps 10min later, only `Again` will be recorded.\n",
        "- The `avg_interval` is the actual average interval after you rate your cards as the `r_history`. It could be longer than the interval given by Anki's built-in scheduler because you reviewed some overdue cards.\n",
        "- The `avg_retention` is the average retention after you press as the `r_history`. `Again` counts as failed recall, and `Hard, Good and Easy` count as successful recall. Retention is the percentage of your successful recall.\n",
        "- The `stability` is the estimated memory state variable, which is an approximate interval that leads to 90% retention.\n",
        "- The `factor` is `stability / previous stability`.\n",
        "- The `group_cnt` is the number of review logs that have the same `r_history`."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "J2IIaY3PDaaG",
        "outputId": "607916c9-da95-48dd-fdab-6bd83fbbbb40"
      },
      "outputs": [
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "349d654b8dbc4a9eadf7d7c6d08b0ba7",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "  0%|          | 0/9793 [00:00<?, ?it/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Trainset saved.\n",
            "Retention calculated.\n"
          ]
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "1c6fa519fad54db1b858aa73ec2fb4ff",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "  0%|          | 0/35283 [00:00<?, ?it/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Stability calculated.\n"
          ]
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "362a374dd868473b9f2e9dc8b3801290",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "analysis:   0%|          | 0/501 [00:00<?, ?it/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Analysis saved!\n",
            "1:again, 2:hard, 3:good, 4:easy\n",
            "first_rating  i         r_history  avg_interval  avg_retention  stability  factor  group_cnt\n",
            "           1  2           (1,3,3)           1.1          0.913        1.4     inf        901\n",
            "           1  3         (1,3,3),3           3.2          0.929        4.2    3.00        759\n",
            "           1  4       (1,3,3),3,3           7.7          0.918        5.9    1.40        624\n",
            "           1  5     (1,3,3),3,3,3          18.3          0.850        6.8    1.15        430\n",
            "           1  6   (1,3,3),3,3,3,3          39.9          0.796       12.9    1.90        256\n",
            "           1  7 (1,3,3),3,3,3,3,3          87.3          0.856       48.9    3.79        142\n",
            "           2  2           (2,3,3)           1.0          0.887        1.0     inf        153\n",
            "           2  3         (2,3,3),3           3.8          0.950        9.5    9.50        125\n",
            "           2  4       (2,3,3),3,3          15.6          0.818        2.9    0.31        101\n",
            "           3  2               (3)           1.0          0.973        5.1     inf        339\n",
            "           3  2             (3,3)           1.0          0.979        7.5     inf       3406\n",
            "           3  2           (3,3,3)           1.0          0.990       16.5     inf        168\n",
            "           3  3             (3),3           1.1          0.950        3.5    0.69        292\n",
            "           3  3           (3,3),3           3.3          0.968       18.9    2.52       3135\n",
            "           3  3         (3,3,3),3           3.0          0.964       14.6    0.88        142\n",
            "           3  4           (3),3,3           3.2          0.976       19.7    5.63        261\n",
            "           3  4         (3,3),3,3           8.6          0.960       19.1    1.01       2867\n",
            "           3  4       (3,3,3),3,3           6.0          0.979       23.0    1.58        132\n",
            "           3  5         (3),3,3,3           7.2          0.969       27.1    1.38        238\n",
            "           3  5       (3,3),3,3,3          19.7          0.939       37.5    1.96       2135\n",
            "           3  5     (3,3,3),3,3,3          13.6          0.947       33.5    1.46        117\n",
            "           3  6       (3),3,3,3,3          19.3          0.952       52.1    1.92        191\n",
            "           3  6     (3,3),3,3,3,3          38.8          0.923       46.0    1.23       1235\n",
            "           3  6   (3,3,3),3,3,3,3          26.2          0.961      102.0    3.04        103\n",
            "           3  7     (3),3,3,3,3,3          39.9          0.865       30.4    0.58        132\n",
            "           3  7   (3,3),3,3,3,3,3          76.4          0.936      121.8    2.65        777\n",
            "           3  8 (3,3),3,3,3,3,3,3         103.5          0.958      257.4    2.11        355\n",
            "           4  2               (4)           3.8          0.965       12.9     inf       1070\n",
            "           4  3             (4),3           8.5          0.967       27.2    2.11        832\n",
            "           4  4           (4),3,3          20.3          0.959       55.6    2.04        704\n",
            "           4  5         (4),3,3,3          46.1          0.930       69.3    1.25        529\n",
            "           4  6       (4),3,3,3,3          96.7          0.942      223.3    3.22        310\n",
            "           4  7     (4),3,3,3,3,3         188.2          0.934      273.7    1.23        145\n"
          ]
        }
      ],
      "source": [
        "analysis = optimizer.create_time_series(\n",
        "    timezone, revlog_start_date, next_day_starts_at)\n",
        "print(analysis)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "k_SgzC-auWmu"
      },
      "source": [
        "## 2 Optimize parameter"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "8E1dYfgQLZAC"
      },
      "source": [
        "### 2.1 Define & Train the model\n",
        "\n",
        "FSRS is a time-series model for predicting memory states.\n",
        "\n",
        "The [./revlog_history.tsv](./revlog_history.tsv) generated before will be used for training the FSRS model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Jht0gneShowU",
        "outputId": "aaa72b79-b454-483b-d746-df1a353b2c8f"
      },
      "outputs": [
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "bcf65eea643a481186a9888ff8039932",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "  0%|          | 0/86120 [00:00<?, ?it/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "[]"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "optimizer.define_model()\n",
        "optimizer.pretrain(verbose=False)\n",
        "optimizer.train(verbose=False, recency_weight=recency_weight)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "BZ4S2l7BWfzr"
      },
      "source": [
        "### 2.2 Result\n",
        "\n",
        "Copy the optimal parameters for FSRS for you in the output of next code cell after running."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "NTnPSDA2QpUu",
        "outputId": "49f487b9-69a7-4e96-b35a-7e027f478fbd"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[0.4783, 1.2172, 9.7398, 15.8796, 6.8942, 0.3659, 3.2729, 0.0099, 1.4107, 0.0061, 0.5899, 1.68, 0.009, 0.4049, 1.2676, 0.0, 3.0064, 0.3535, 0.5764, 0.2246, 0.2205]\n"
          ]
        }
      ],
      "source": [
        "print(optimizer.w)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "<font color=orange>Note: These values should be used with build-in FSRS of Anki 23.12 or custom scheduling script of FSRS4Anki v4.11.0</font>"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### 2.3 Preview"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {
        "id": "I_zsoDyTaTrT"
      },
      "source": [
        "You can see the memory states and intervals generated by FSRS as if you press the good in each review at the due date scheduled by FSRS."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "iws4rtP1WKBT",
        "outputId": "890d0287-1a17-4c59-fbbf-ee54d79cd383"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "1:again, 2:hard, 3:good, 4:easy\n",
            "\n",
            "first rating: 1\n",
            "rating history: (1,3,3),3,3,3,3,3,3,3,3\n",
            "interval history: 0.0d,0.0d,0.0d,1.0d,2.0d,4.0d,8.0d,16.0d,1.1m,2.2m,4.5m\n",
            "factor history: 0.0,0.0,0.0,0.0,2.00,2.00,2.00,2.00,2.06,2.03,2.03\n",
            "difficulty history: 0,6.9,6.9,6.9,6.8,6.8,6.8,6.8,6.8,6.7,6.7\n",
            "stability history: 0,0.5,0.7,0.9,1.9,4.0,8.1,16.4,33.0,67.2,136.5\n",
            "\n",
            "first rating: 2\n",
            "rating history: (2,3,3),3,3,3,3,3,3,3,3\n",
            "interval history: 0.0d,0.0d,0.0d,2.0d,4.0d,8.0d,17.0d,1.2m,2.6m,5.6m,11.8m\n",
            "factor history: 0.0,0.0,0.0,0.0,2.00,2.00,2.12,2.18,2.11,2.14,2.13\n",
            "difficulty history: 0,6.5,6.4,6.4,6.4,6.4,6.4,6.4,6.3,6.3,6.3\n",
            "stability history: 0,1.2,1.4,1.6,3.8,8.3,17.4,36.7,78.5,166.6,354.6\n",
            "\n",
            "first rating: 3\n",
            "rating history: (3,3),3,3,3,3,3,3,3,3,3\n",
            "interval history: 0.0d,0.0d,10.0d,22.0d,1.7m,3.8m,8.7m,1.6y,3.6y,8.2y,18.4y\n",
            "factor history: 0.0,0.0,0.0,2.20,2.32,2.25,2.26,2.26,2.25,2.25,2.25\n",
            "difficulty history: 0,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.7,5.7\n",
            "stability history: 0,9.7,9.7,22.4,50.5,115.0,260.3,587.7,1325.4,2982.6,6700.7\n",
            "\n",
            "first rating: 4\n",
            "rating history: (4),3,3,3,3,3,3,3,3,3,3\n",
            "interval history: 0.0d,16.0d,1.3m,3.3m,8.2m,1.7y,4.1y,10.0y,24.5y,59.8y,100.0y\n",
            "factor history: 0.0,0.0,2.50,2.48,2.47,2.47,2.46,2.45,2.45,2.44,1.67\n",
            "difficulty history: 0,4.9,4.9,4.9,4.9,4.9,4.9,4.9,4.9,4.9,4.9\n",
            "stability history: 0,15.9,39.8,99.1,245.5,605.8,1491.5,3660.4,8952.3,21825.2,36500.0\n",
            "\n"
          ]
        }
      ],
      "source": [
        "requestRetention = 0.9  # recommended setting: 0.8 ~ 0.9\n",
        "\n",
        "preview = optimizer.preview(requestRetention)\n",
        "print(preview)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "You can change the `test_rating_sequence` to see the scheduling intervals in different ratings."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "rating history: 3,3,3,3,3,1,1,3,3,3,3,3\n",
            "interval history: 0.0d,10.0d,22.0d,1.7m,3.8m,8.6m,16.0d,4.0d,5.0d,7.0d,10.0d,13.0d,18.0d\n",
            "factor history: 0.0,0.0,2.20,2.27,2.28,2.26,0.06,0.25,1.25,1.40,1.43,1.30,1.38\n",
            "difficulty history: 0,5.8,5.8,5.8,5.8,5.8,8.8,9.6,9.6,9.5,9.5,9.4,9.4\n"
          ]
        }
      ],
      "source": [
        "test_rating_sequence = \"3,3,3,3,3,1,1,3,3,3,3,3\"\n",
        "requestRetention = 0.9  # recommended setting: 0.8 ~ 0.9\n",
        "\n",
        "preview_sequence = optimizer.preview_sequence(\n",
        "    test_rating_sequence, requestRetention)\n",
        "print(preview_sequence)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### 2.4 Predict memory states and distribution of difficulty\n",
        "\n",
        "Predict memory states for each review group and save them in [./prediction.tsv](./prediction.tsv).\n",
        "\n",
        "Meanwhile, it will count the distribution of difficulty."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "difficulty\n",
              "1     0.080504\n",
              "2     0.017812\n",
              "3     0.021691\n",
              "4     0.036008\n",
              "5     0.051463\n",
              "6     0.215490\n",
              "7     0.087622\n",
              "8     0.053228\n",
              "9     0.170297\n",
              "10    0.265885\n",
              "Name: count, dtype: float64"
            ]
          },
          "execution_count": 8,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "optimizer.predict_memory_states()"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "authorship_tag": "ABX9TyMnk8/Ih2JAJZJ1PBkXQUBC",
      "collapsed_sections": [],
      "provenance": [],
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "fsrs4anki",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.9.16"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
